/*
 * Unidata Platform Community Edition
 * Copyright (c) 2013-2020, UNIDATA LLC, All rights reserved.
 * This file is part of the Unidata Platform Community Edition software.
 *
 * Unidata Platform Community Edition is free software: you can redistribute it and/or modify
 * it under the terms of the GNU General Public License as published by
 * the Free Software Foundation, either version 3 of the License, or
 * (at your option) any later version.
 *
 * Unidata Platform Community Edition is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 * GNU General Public License for more details.
 *
 * You should have received a copy of the GNU General Public License
 * along with this program. If not, see <https://www.gnu.org/licenses/>.
 */

package org.unidata.mdm.data.service.segments.relations.restore;

import java.util.Date;
import java.util.List;
import java.util.Objects;

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;
import org.unidata.mdm.core.type.data.RecordStatus;
import org.unidata.mdm.core.type.timeline.TimeInterval;
import org.unidata.mdm.core.type.timeline.Timeline;
import org.unidata.mdm.data.context.RelationRestoreContext;
import org.unidata.mdm.data.dto.RestoreRelationDTO;
import org.unidata.mdm.data.exception.DataExceptionIds;
import org.unidata.mdm.data.exception.DataProcessingException;
import org.unidata.mdm.data.module.DataModule;
import org.unidata.mdm.data.service.DataRecordsService;
import org.unidata.mdm.data.service.impl.CommonRelationsComponent;
import org.unidata.mdm.data.service.impl.RelationDraftProviderComponent;
import org.unidata.mdm.data.service.segments.ContainmentRelationSupport;
import org.unidata.mdm.data.type.apply.RelationRestoreChangeSet;
import org.unidata.mdm.data.type.data.OriginRelation;
import org.unidata.mdm.data.type.data.RelationType;
import org.unidata.mdm.data.type.keys.RelationKeys;
import org.unidata.mdm.system.type.pipeline.Start;

/**
 * @author Mikhail Mikhailov on Nov 24, 2019
 */
@Component(RelationRestoreStartExecutor.SEGMENT_ID)
public class RelationRestoreStartExecutor extends Start<RelationRestoreContext, RestoreRelationDTO> implements ContainmentRelationSupport {
    /**
     * The logger.
     */
    private static final Logger LOGGER = LoggerFactory.getLogger(RelationRestoreStartExecutor.class);
    /**
     * This segment ID.
     */
    public static final String SEGMENT_ID = DataModule.MODULE_ID + "[RELATION_RESTORE_START]";
    /**
     * Localized message code.
     */
    public static final String SEGMENT_DESCRIPTION = DataModule.MODULE_ID + ".relation.restore.start.description";
    /**
     * Common rel component.
     */
    @Autowired
    private CommonRelationsComponent commonRelationsComponent;
    /**
     * The DRS.
     */
    @Autowired
    private DataRecordsService dataRecordsService;
    /**
     * RDPC.
     */
    @Autowired
    private RelationDraftProviderComponent relationDraftProviderComponent;
    /**
     * Constructor.
     */
    public RelationRestoreStartExecutor() {
        super(SEGMENT_ID, SEGMENT_DESCRIPTION, RelationRestoreContext.class, RestoreRelationDTO.class);
    }
    /**
     * {@inheritDoc}
     */
    @Override
    public void start(RelationRestoreContext ctx) {
        setup(ctx);
    }

    /**
     * {@inheritDoc}
     */
    @Override
    public String subject(RelationRestoreContext ctx) {
        setup(ctx);
        RelationKeys keys = ctx.relationKeys();
        return keys.getRelationName();
    }

    /**
     * {@inheritDoc}
     */
    protected void setup(RelationRestoreContext ctx) {

        if (ctx.setUp()) {
            return;
        }

        // Fetch or create timeline and keys
        setupTimelineAndKeys(ctx);

        // Simple check
        setupVerify(ctx);

        // Create or set other fields if needed
        setupFields(ctx);

        // Containment
        setupContainment(ctx);

        ctx.setUp(true);
    }

    protected void setupTimelineAndKeys(RelationRestoreContext iCtx) {

        // Check the timeline presence and load it, if needed.
        // Timeline may be already set by the connector.
        Timeline<OriginRelation> current = iCtx.currentTimeline();
        if (Objects.isNull(current)) {

            current = commonRelationsComponent.ensureAndGetRelationTimeline(iCtx);
            if (iCtx.isPeriodRestore()) {
                current = current.reduceBy(iCtx.getValidFrom(), iCtx.getValidTo());
            }

            iCtx.currentTimeline(current);
            iCtx.keys(current.getKeys());
        }
    }

    protected void setupVerify(RelationRestoreContext iCtx) {

        // 1. Rel existence
        Timeline<OriginRelation> timeline = iCtx.currentTimeline();
        if (Objects.isNull(timeline) || Objects.isNull(timeline.getKeys())) {

            final String message
                = "Relation restore: relation of type [{}] not found by supplied keys - relation etalon id [{}], relation origin id [{}], "
                + "etalon id: [{}], origin id [{}], external id [{}], source system [{}], name [{}]";

            String relationName = iCtx.relationName();
            String relationEtalonKey = iCtx.getRelationEtalonKey();
            String relationOriginKey = iCtx.getRelationOriginKey();
            String recordEtalonKey = iCtx.getEtalonKey();
            String recordOriginKey = iCtx.getOriginKey();
            String recordExternalKey = iCtx.getExternalId();
            String recordSourceSystem = iCtx.getSourceSystem();
            String recordEntityName = iCtx.getEntityName();

            LOGGER.warn(message,
                    relationName,
                    relationEtalonKey,
                    relationOriginKey,
                    recordEtalonKey,
                    recordOriginKey,
                    recordExternalKey,
                    recordSourceSystem,
                    recordEntityName);

            throw new DataProcessingException(message, DataExceptionIds.EX_DATA_RELATIONS_RESTORE_NOT_FOUND_BY_SUPPLIED_KEYS,
                    relationName,
                    relationEtalonKey,
                    relationOriginKey,
                    recordEtalonKey,
                    recordOriginKey,
                    recordExternalKey,
                    recordSourceSystem,
                    recordEntityName);
        }

        RelationKeys keys = timeline.getKeys();

        // 2. Etalon is active, discard restore
        if (!iCtx.isPeriodRestore() && keys.getEtalonKey().getStatus() != RecordStatus.INACTIVE) {
            final String message = "Relation etalon [ID: {}] is active. Restore rejected.";
            LOGGER.warn(message, keys.getEtalonKey().getId());
            throw new DataProcessingException(message, DataExceptionIds.EX_DATA_RELATIONS_RESTORE_ETALON_ACTIVE, keys.getEtalonKey().getId());
        }

        // 3. Etalon is inactive, discard period restore
        if (iCtx.isPeriodRestore() && keys.getEtalonKey().getStatus() != RecordStatus.ACTIVE) {
            final String message = "Etalon [ID: {}] is inactive. Period restore rejected.";
            LOGGER.warn(message, keys.getEtalonKey().getId());
            throw new DataProcessingException(message, DataExceptionIds.EX_DATA_RELATIONS_RESTORE_PERIOD_INACTIVE, keys.getEtalonKey().getId());
        }

        // 4. Check ability to restore record.
        if (iCtx.isPeriodRestore()) {

            List<TimeInterval<OriginRelation>> selection = timeline.selectBy(iCtx.getValidFrom(), iCtx.getValidTo());
            if (selection.isEmpty()) {
                throw new DataProcessingException("Cannot restore relation period. No intervals exist for from [{}] and to [{}].",
                        DataExceptionIds.EX_DATA_RELATIONS_RESTORE_EMPTY_PERIOD, iCtx.getValidFrom(), iCtx.getValidTo());
            }
        }
    }

    protected void setupFields(RelationRestoreContext iCtx) {

        Timeline<OriginRelation> timeline = iCtx.currentTimeline();
        RelationKeys keys = timeline.getKeys();

        // Name and type not really needed. Added just for convenience.
        if (Objects.isNull(iCtx.relationKeys())) {
            iCtx.relationKeys(keys);
        }

        iCtx.relationName(keys.getRelationName());
        iCtx.relationType(keys.getRelationType());

        if (Objects.isNull(iCtx.timestamp())) {
            iCtx.timestamp(new Date());
        }

        // May be already set by batch
        if (Objects.isNull(iCtx.changeSet())) {
            RelationRestoreChangeSet set = new RelationRestoreChangeSet();
            set.setRelationType(keys.getRelationType());
            iCtx.changeSet(set);
        }
    }

    protected void setupContainment(RelationRestoreContext iCtx) {

        if (iCtx.relationType() != RelationType.CONTAINS) {
            return;
        }

        restore(iCtx);
    }
    /**
     * {@inheritDoc}
     */
    @Override
    public RelationDraftProviderComponent draftProviderComponent() {
        return relationDraftProviderComponent;
    }
    /**
     * {@inheritDoc}
     */
    @Override
    public DataRecordsService dataRecordsService() {
        return dataRecordsService;
    }
}
